/**
 * 
 */
package com.wgo.bpot.client.session;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.logging.Logger;

import com.wgo.bpot.client.servicerequest.UserSessionContainer;
import com.wgo.bpot.common.transport.exception.db.DatabaseException;
import com.wgo.bpot.common.transport.servicefacade.BpotServices;
import com.wgo.bpot.common.transport.servicefacade.DispatchedModifyingMethodsTransport;
import com.wgo.bpot.common.transport.servicefacade.DomainServerService;
import com.wgo.bpot.common.transport.servicefacade.RetrieveResult;
import com.wgo.bpot.common.transport.servicefacade.SaveResult;
import com.wgo.bpot.common.transport.util.MethodNamingConvention;
import com.wgo.bpot.domain.common.CommandHistory;
import com.wgo.bpot.domain.common.UserSession;
import com.wgo.bpot.wiring.common.DomainModelConvention;



/**
 * @author petterei  
 */
public class LightSession<T, S extends BpotServices> implements Session<T> {
	
	private static final Logger log = Logger.getLogger(LightSession.class.getSimpleName());
	
	public static final String ALL_SERVICES_SUB_URL = "/service";
	public static final String SERVICE_SUB_URL = "/lightservice";
	public static final String DOMAIN_SERVER_SERVICE_SUB_URL = "/domainServerService";
	public static final int DEFAULT_UPDATE_DEPTH = 1;
	public static final String CLIENT_VERSION = "0.2.1";

	private Map<Class<? extends T>, DomainProxy<? extends T>> rootSystems = null;
	private S services;
	private DomainServerService domainServerService;
	private URL serverContextUrl;
//	private boolean isAnyDirty = false;
	private Set<Serializable> dirtyConceptIds = null ;
	private int initLinkDepth = 1;
	
	private DomainModelConvention<T> domainModelConvention;
//    private MemoryUtil memory = new MemoryUtil();

    
    public LightSession( S services, DomainModelConvention<T> domainModelConvention , URL remoteUrl){
        this.domainModelConvention = domainModelConvention;
        this.services = services;
        this.serverContextUrl = remoteUrl;
////        log.info("Memory usage when starting client: " + memory.getInitialUsedMemory() + " kb.");
        MethodNamingConvention.PROXY_SUPER_INTERFACE = domainModelConvention.getDomainModelInterface();
        RemoteConceptFactory.PROXY_SUPER_INTERFACE = domainModelConvention.getDomainModelInterface();
        DomainProxyInvocationHandler.init();
        this.domainServerService = services;
        rootSystems = null;
        dirtyConceptIds = new HashSet<Serializable>();
        initLinkDepth = 1;
    }
    
    public UserSession getUserSession() {
        return ((UserSessionContainer)services).getUserSession();
//        return serviceRequestor.getUserSession();
    }
    
	/**
	 * Please notice that the return result is an Object array of HashMaps, while it really should be an Object array containing a single element: RootConceptImpl 
	 */
	private void registerRootConcept() {
		rootSystems = new HashMap<Class<? extends T>, DomainProxy<? extends T>>();
		for (Class<? extends T> rootConceptType : domainModelConvention.getRootConceptClasses()) {
			RetrieveResult<? extends T> rs = services.retrieveRootSystem(initLinkDepth,rootConceptType);
			T system = rs.getDomainConcept(); //service.retrieveRootSystem(initLinkDepth,rootConceptClass);
			rootSystems.put((Class<T>)system.getClass().getInterfaces()[0], DomainProxyInvocationHandler.<T>newInstance(system, this, domainModelConvention.getIdentifierMethod(system) , initLinkDepth, rs.getServerTime()));			
		}
	}
	
	private synchronized Map<Class<? extends T>, DomainProxy<? extends T>> getRootSystemsInternal(){
		if (null == rootSystems) {
			registerRootConcept();
		}
		return rootSystems;
	}
	
	
	
	/* (non-Javadoc)
	 * @see com.wgo.bpot.client.session.Session#getRootSystems()
	 */
	@SuppressWarnings("unchecked")
	public <R extends T> Collection<R> getRootSystems() {
		return (Collection<R>)(Collection)getRootSystemsInternal().values(); //FIXME: needs double cast at the current time to trick sun compiler
	}

	/**
	 * @return Returns the remoteUrl.
	 */
	public URL getServerContextUrl() {
		return serverContextUrl;
	}
	
	/**
	 * @return Returns the rootProxy.
	 */
	@SuppressWarnings("unchecked")
	public <R extends T> R getRootSystem(Class<R> rootSystemType) {
        R result = (R)getRootSystemsInternal().get(rootSystemType);
//        memory.increaseCounter("getRootSystem");
        return result;
	}
	
	/**
	 * Saves all new or modified concepts.
	 */
	@SuppressWarnings("unchecked")
	public synchronized void save() {
		Map<DispatchedModifyingMethodsTransport, DomainProxy> dispatchedModifyingMethods = new Hashtable<DispatchedModifyingMethodsTransport, DomainProxy>();
		HashSet<DomainProxy> backTrace = new HashSet<DomainProxy>();
		for (DomainProxy rootSystem : getRootSystemsInternal().values()) {
			rootSystem.getDispatchedModifyingMethods(dispatchedModifyingMethods, rootSystem, backTrace);
		}
		if (0 < dispatchedModifyingMethods.size()) {
			int i = 0;
			DispatchedModifyingMethodsTransport[] dispatchedModifyingMethodsArray = new DispatchedModifyingMethodsTransport[dispatchedModifyingMethods.size()];
			DomainProxy[] proxies = new DomainProxy[dispatchedModifyingMethods.size()];
			for (Entry<DispatchedModifyingMethodsTransport, DomainProxy> entry : dispatchedModifyingMethods.entrySet()) {
				dispatchedModifyingMethodsArray[i] = entry.getKey();
				proxies[i] = entry.getValue();
				i++;
			}
			SaveResult saveResult = services.save(dispatchedModifyingMethodsArray);
			Object[] savedConcepts = saveResult.getSavedConcepts();
			for (int j = 0 ; j < dispatchedModifyingMethodsArray.length ; j++ ) {
				proxies[j].updateFromPersistentStore(savedConcepts[j], proxies[j],saveResult.getServerTime());
			}
			for (DomainProxy proxy : proxies) {
				proxy.cleanUpAfterSave(proxy);
			}
		} else if (isAnyDirty()) {
			log.finer("Session was marked dirty, but no dirty concepts were found. This is probably caused by inverse operations.");	
		}
        dirtyConceptIds = new HashSet<Serializable>(); //TODO: perhaps let the proxies themselves fix this? (IE: remove them selves as soon as they are updated?
//        memory.increaseCounter("save");
	}
	
	public boolean isDirty(Object persistent) {
		if (persistent instanceof DomainProxy) {
			return ((DomainProxy)persistent).isDirty();
		}
		throw new DomainSessionException("Object was not proxy!");
	}
	
	public boolean isAnyDirty() {
		return ! dirtyConceptIds.isEmpty();
	}
	
	/**
	 * Initializes a new domain model concept.
	 * 
	 * @see com.wgo.bpot.common.transport.util.DomainModelTypeNamingConvention
	 * 
	 * Examples:
	 * Project project = createConcept(Project.class);
	 * Category category = createConcept(Category.class);
	 * Requirement requirement = createConcept(Requirement.class);
	 * Stakeholder stakeholder = createConcept(Stakeholder.class);
	 * PropertySet project = createConcept(PropertySet.class);
	 * PropertyDef propertyDef = createConcept(PropertyDef.class, PropertyType.TYPES[1]);
	 * PropertyValue propertyValue = createConcept(PropertyValue.class, propertyDef);
	 * 
	 * @param <T> The type of the new concept
	 * @param clazz The class of the instance. Note this has to be a known class
	 * 				located in the domain model package {@link com.wgo.bpot.common.transport.util.DomainModelTypeNamingConvention}
	 * @param args Possible types to be set on the object. Should be of concept types. 
	 * @return The new concept instance
	 */
	@SuppressWarnings("unchecked")
	public <R extends T> R createConcept(Class<R> clazz, Object... args) {
		R actualConcept = null;
		Class<R> implClass = (Class<R>)domainModelConvention.apiToImpl(clazz) ; // DomainModelTypeNamingConvention.loadImplClass(clazz);//, DomainModelTypeNamingConvention.getModelImplementationPackage(), DomainModelTypeNamingConvention.MODEL_IMPLEMENTATION_TYPE_SUFFIX);
		Map<Method,Object> methods = new Hashtable<Method,Object>();
		for (Object arg : args) {
			Class argClass = null;
			try { // EXTENSION: could be extended to handle more types of arguments.
				argClass = arg.getClass();
				while (argClass.isAnonymousClass()) { // could probably change from 'while' to 'if'
					argClass = argClass.getSuperclass();
				}
				methods.put(MethodNamingConvention.retrieveMethod("", MethodNamingConvention.SET_METHOD.getPrefix() + argClass.getInterfaces()[0].getSimpleName(), implClass, argClass.getInterfaces()[0]), arg);
			} catch (Exception e) {
				log.warning("Could not find method to set argument " + arg + " type: " + argClass.getInterfaces()[0].getSimpleName());
			}
		}
		try {
			if (0 < args.length) { // does not trigger fields to be updated at the server.. (but does verify that such parameter is legal to set)
				Class argClass = args[0].getClass();
				while (argClass.isAnonymousClass()) { // could probably change from 'while' to 'if'
					argClass = argClass.getSuperclass();
				}
				Constructor<R> constructor = implClass.getConstructor(argClass.getInterfaces()[0]);
				actualConcept = constructor.newInstance(new Object[]{args[0]});
			} else {
				actualConcept = implClass.newInstance();	
			}
		} catch (Exception e) {
			throw new DomainSessionException("Could not instantiate concept of type: " + implClass, e);
		}
		R proxy = (R) DomainProxyInvocationHandler.<R>newInstance(actualConcept, this, domainModelConvention.getIdentifierMethod(actualConcept), 0);
		for (Entry<Method,Object> entry : methods.entrySet()) {
			((DomainProxy)proxy).putDirtyMethod(entry.getKey(), entry.getValue(), MethodNamingConvention.getMethodType(entry.getKey()).retrieveDefaultReturnValue(entry.getKey(), entry.getValue()));
		}
//        memory.increaseCounter("createConcept(" + clazz.getSimpleName() + ")");
		return proxy;
	}
	
	protected boolean markDirty(Serializable conceptId) {
        return dirtyConceptIds.add(conceptId);
	}

	protected boolean unMarkDirty(Serializable conceptId) {
        return dirtyConceptIds.remove(conceptId);
	}

	protected S getServices() {
		return services;
	}

	protected DomainServerService getDomainServerService() {
		return domainServerService;
	}

	/**
	 * Only made available for testing purposes. This should ideally be factored out of
	 * this class. It would be a disaster if clients started to recreate the DB schema whenever they please...!
	 */
	public void createDbSchema() throws DomainSessionException {
		try {
			services.createDbSchema(domainModelConvention.getRootConceptClasses().iterator().next()); //FIXME: remove parameter or use it in service implementation
		} catch (DatabaseException e) {
			throw new DomainSessionException(e);
		}
	}

	public Set<Class<? extends T>> getRootConceptClasses() {
		return domainModelConvention.getRootConceptClasses();
	}
    
	public Date getServerTime() {
		return new Date(services.getServerTime());
	}

	public List<CommandHistory> retrieveCommandHistory(Object persistentConcept) {
		if (null != persistentConcept) {
			Object p = ((DomainProxy)persistentConcept).getActualDomainModelObject();
			return services.retrieveCommandHistory(p);
		}
//        memory.increaseCounter("retrieveCommandHistory");
		return services.retrieveCommandHistory(null);
	}

}
